<?php
/**
 * Created by PhpStorm.
 * User: Linh <klinson@163.com>
 * Date: 2025/3/20
 * Time: 12:15
 */

use Illuminate\Support\Facades\Log;


/**
 * openai发起请求
 * @param $url
 * @param $data
 * @param $auth
 * @param $headers
 * @return mixed
 */
function openai_request($url, $data = [], $auth = null, $method = 'POST', $headers = [], $timeout = 600, $stream = false)
{
    $headers[] = 'Content-Type: application/json';
    if ($auth) $headers[] = 'Authorization: Bearer '.$auth;

    $curl = curl_init();

    curl_setopt_array($curl, array(
        CURLOPT_URL => $url,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_ENCODING => '',
        CURLOPT_MAXREDIRS => 10,
        CURLOPT_TIMEOUT => $timeout,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_POSTFIELDS => json_encode($data),
        CURLOPT_HTTPHEADER => $headers,
    ));
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);    // https请求时要设置为false 不验证证书和hosts  FALSE 禁止 cURL 验证对等证书（peer's certificate）, 自cURL 7.10开始默认为 TRUE。从 cURL 7.10开始默认绑定安装。
    curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0);//检查服务器SSL证书中是否存在一个公用名(common name)。

    if ($stream) {
        $stream_output = [
            'id' => '',
            'object' => '',
            'created' => 0,
//            'chunks' => [],
            'choices' => [
                [
                    'index' => 0,
                    'message' => [
                        'role' => 'assistant',
                        'content' => '',
                        'reasoning_content' => '',
                    ],
                    'finish_reason' => 'stop'
                ]
            ],
            'usage' => [
                'prompt_tokens' => 0,
                'completion_tokens' => 0,
                'total_tokens' => 0,
            ],
        ];

        // 流式则将结果收集起来
        $func = function ($ch, $data) use (&$stream_output) {
//            dump($data);
            $chunks = explode("\n", $data);
            foreach ($chunks as $chunk) {
                $chunk = trim($chunk);
                if (!$chunk) continue;
//                file_put_contents('ai.txt', $chunk.PHP_EOL, FILE_APPEND);
//                $stream_output['chunks'][] = $chunk;
                $res = preg_replace('/^data:/iu', '', $chunk);
                if (! $res) continue ;
                $res = json_decode($res, true);
                if (! $res) continue;
                if (($res['error'] ?? '') || ($res['errors'] ?? '')) {
                    $stream_output = $res;
                    break;
                } else {
                    if (isset($res['choices'][0]['delta']['reasoning_content'])) $stream_output['choices'][0]['message']['reasoning_content'] .= $res['choices'][0]['delta']['reasoning_content'];
                    if (isset($res['choices'][0]['delta']['content'])) $stream_output['choices'][0]['message']['content'] .= $res['choices'][0]['delta']['content'];

                    // 结束标记
                    if (($res['choices'][0]['finish_reason'] ?? '') == 'stop') {
                        $stream_output['id'] = $res['id'];
                        $stream_output['object'] = $res['object'];
                        $stream_output['created'] = $res['created'];
                        $stream_output['usage']['prompt_tokens'] = $res['usage']['prompt_tokens'];
                        $stream_output['usage']['completion_tokens'] = $res['usage']['completion_tokens'];
                        $stream_output['usage']['total_tokens'] = $res['usage']['total_tokens'];
                    }
                }
            }

            return strlen($data);
        };
        curl_setopt($curl, CURLOPT_WRITEFUNCTION, $func);
    }

    try {
        $response = curl_exec($curl);
//        dump($response);
    } catch (\Exception $exception) {
//        dump(exception_to_string($exception));
        return false;
    }
    if ($stream) $response = json_encode($stream_output);

    curl_close($curl);
    return $response;

    //{"choices":[{"message":{"role":"assistant","content":"我是Qwen，由阿里云开发的大规模语言模型。我被设计用来理解和生成自然语言文本，能够回答问题、创作文字，比如写故事、写公文、写邮件、写剧本等等，还能表达观点、撰写代码。如果您有任何问题或需要帮助，请随时告诉我！"},"finish_reason":"stop","index":0,"logprobs":null}],"object":"chat.completion","usage":{"prompt_tokens":20,"completion_tokens":62,"total_tokens":82},"created":1733466539,"system_fingerprint":null,"model":"qwen2.5-coder-32b-instruct","id":"chatcmpl-6263d10d-0050-9c56-b78e-3f71f561a77a"}
}

/**
 * openai 对话
 * @param $url
 * @param $model
 * @param $content
 * @param $auth
 * @return mixed
 */
function openai_chat($url, $model, $content, $auth = null, $messages = [], $timeout = 600, $stream = false)
{
    $messages[] = [
        'role' => 'user',
        'content' => $content
    ];
    $data = [
        'model' => $model,
        'messages' => $messages,
        "stream" => $stream ? true : false,
        'max_tokens' => 1024*5,
    ];

    $res_str = openai_request($url, $data, $auth, 'POST', [], $timeout, $stream);
    if (empty($res_str)) return false;
    $res = json_decode($res_str, true);
    if (! $res) $res = format_json($res_str); // DeepSeek-V3-0324有时候会返回2个json
    if (! $res) {
        // DeepSeek-V3-0324有时候会返回多个json
        $tmp = explode('}{"', $res_str);
        $res_str2 = '{"'.end($tmp);
        $res = json_decode($res_str2, true);
        if (! $res) $res = format_json($res_str2);
    }

    $response = $res['choices'][0]['message']['content'] ?? ($res[0]['message']['content'] ?? '');
    $think = $res['choices'][0]['message']['reasoning_content'] ?? ($res[0]['message']['reasoning_content'] ?? '');
    if (empty($response)) {
        $error_code = $res['error']['code'] ?? '';
        $error_msg = $res['errors']['message'] ?? '';
        if (!preg_in_array(['504 Gateway Time-out', '502 Bad Gateway', 'data_inspection_failed', 'internal_server_error'], $res_str)) {
            if (preg_in_array(['Request limit exceeded', 'insufficient_quota', 'limit_requests'], $res_str)) {
                throw new \Exception('DAY_LIMIT:' . $res_str);
            } else if (preg_in_array(['Authenticaton failed', '未提供令牌'], $res_str)) {
                throw new \Exception('LOGIN:' . $res_str);
            } else if (preg_in_array(['对网站造成安全威胁', 'bad_response_status_code', 'invalid_request_error'], $res_str)) {
                if (is_console()) sleep(5);
                throw new \Exception('WARINING:' . $res_str);
            } else if ((isset($res['choices'][0]['message']['content']) || isset($res[0]['message']['content'])) && isset($res['usage']['total_tokens']) && $res['usage']['total_tokens'] == 0) {
                // 不报错，就是返回空信息 {"id":"","object":"","created":0,"choices":[{"index":0,"message":{"role":"assistant","content":"","reasoning_content":""},"finish_reason":"stop"}],"usage":{"prompt_tokens":0,"completion_tokens":0,"total_tokens":0}}
            } else {
                throw new \Exception('ERROR:'.$res_str);
            }
        }
        return false;
    }
    return [
        'answer' => $response,
        'think' => $think,
    ];
}

/**
 * openai模型查询
 * @param $url
 * @return array
 */
function openai_models($url, $auth = null)
{
    $response = openai_request($url, [], $auth, 'GET');
    $response = json_decode($response, true);
    $list = array_column($response['data'] ?? [], 'id');
    return $list;
}

function modelscope_models()
{
    $url = 'https://api-inference.modelscope.cn/v1/models';
    $list = openai_models($url);

    $account_key = 'modelscope_api';
    $list = array_map(function ($item) use ($account_key) {
        return $account_key.'/'.$item;
    }, $list);
    return $list;
}

function modelscope_chat($content, $model_name, $messages = [], $timeout = 600)
{
    $url = 'https://api-inference.modelscope.cn/v1/chat/completions';
    $model_name = preg_replace('/^ms_api\//iu', '', $model_name);
    if (preg_match('/(.*?)\/(no_think|think)$/iu', $model_name, $match)) {
        // 格式化出正确的模型，拼上思考控制参数
        $model_name = $match[1];
        $content .= '/'.$match[2];
    }

    // DeepSeek-R1模型下架
    if ($model_name == 'deepseek-ai/DeepSeek-R1') $model_name = 'deepseek-ai/DeepSeek-R1-0528';
    if ($model_name == 'deepseek-ai/DeepSeek-V3-0324' || $model_name == 'deepseek-ai/DeepSeek-V3') $model_name = 'deepseek-ai/DeepSeek-V3.1';

    $account_key = 'modelscope_api';
    $auths = config('services.'.$account_key);
    $auth = $auths ? (is_array($auths) ? random_array($auths) : $auths) : null;
    if (!$auth) return false;
    try {
        $res = openai_chat($url, $model_name, $content, $auth, $messages, $timeout, false);
        $answer = $res['answer'] ?? '';
        $think = $res['think'] ?? '';
        if (empty($answer)) {
            return false;
        }
        return [
            'answer' => $answer,
            'questions' => [],
            'think' => $think ? [$think] : [],
        ];
    } catch (\Exception $exception) {
        Log::error("modelscope_chat失败[{$auth}]：".exception_to_string($exception));
    }
    return false;
}

/**
 * deepseek
 * @param $content
 * @param $model_name
 * @param $timeout
 * @return array|false
 * @link https://api-docs.deepseek.com/zh-cn/
 */
function deepseek_chat($content, $model_name, $messages = [], $timeout = 1000)
{
    $url = 'https://api.deepseek.com/chat/completions';
    $model_name = preg_replace('/^deepseek_api\//iu', '', $model_name);

    $account_key = 'deepseek_api';
    $auth = random_array(config('services.'.$account_key));
    if (empty($auth)) {
        return false;
    }
    try {
        $res = openai_chat($url, $model_name, $content, $auth, $messages, $timeout, false);
        $answer = $res['answer'] ?? '';
        $think = $res['think'] ?? '';
        if (empty($answer)) {
            return false;
        }
        return [
            'code' => 200,
            'data' => [
                'answer' => $answer,
                'questions' => [],
                'think' => $think ? [$think] : [],
            ],
            'msg' => '',
        ];
    } catch (\Exception $exception) {
        Log::error("deepseek_chat失败[{$auth}]：".exception_to_string($exception));
    }
    return false;
}

function bitahub_models()
{
    $url = 'https://openai.bitahub.com/v1/models';
    $account_key = 'bitahub_api';
    $auth = array_rand(config('services.'.$account_key));
    $list = openai_models($url, $auth);

    $list = array_map(function ($item) use ($account_key) {
        return $account_key.'/'.$item;
    }, $list);
    return $list;
}

function bitahub_chat($content, $model_name, $timeout = 300)
{
    $url = 'https://openai.bitahub.com/v1/chat/completions';
    $model_name = preg_replace('/^bitahub_api\//iu', '', $model_name);

    $account_key = 'bitahub_api';
    $auth = array_rand(config('services.'.$account_key));
    try {
        $answer = openai_chat($url, $model_name, $content, $auth, $timeout);
        if (empty($answer)) {
            return false;
        }
        $think = [];
        if (preg_in_array(['deepseek-r1'], $model_name)) {
            // 提取think
            if (preg_match('/<think>([\S\s]*?)<\/think>([\S\s]*)/iu', $answer, $match)) {
                if (count($match) == 3) {
                    $think = [trim($match[1])];
                    $answer = $match[2];
                }
            }
        }
        return [
            'code' => 200,
            'data' => [
                'answer' => trim($answer),
                'questions' => [],
                'think' => $think,
            ],
            'msg' => '',
        ];
    } catch (\Exception $exception) {
    }
    return false;
}

/**
 * 格式化json
 * @param $content
 * @return mixed
 * @author klinson <klinson@163.com>
 */
function format_json($content, $filter_br = true)
{
    if (! is_string($content)) return $content;
    if ($data = json_decode($content, true)) return $data;
    if ($filter_br) {
        $content = str_replace("\n", "", $content);
        $content = str_replace('\n\n', '\n', $content);
        $content = preg_replace('/[ ]{2,}/iu', ' ', $content);
    }
    $data = json_decode($content, true);
    if (is_null($data)) $data = json_decode(str_replace("'", '"', $content), true);

    if (is_null($data)) {
        $patterns = [
            '/\[([\s\S]+)\]/ium',
            '/{([\s\S]+)}/ium',
            '/\[((?:(?!```).)*)\]/ium',
            '/{((?:(?!```).)*)}/ium',
            '/\[((?:(?!(```|##)).)*)\]/ium',
            '/{((?:(?!(```|##)).)*)}/ium'
        ];
        $res = null;
        $res_length = 0;
        foreach ($patterns as $pattern) {
            if (preg_match_all($pattern, $content, $matchs)) {
//                rsort($matchs);
//                foreach ($matchs[0] as $match) {
                $match = end($matchs[0]);
                $match_length = mb_strlen($match);
                if ($match_length <= $res_length) continue;
                $data = json_decode($match, true);
                if (is_null($data) && mb_stripos($match, "'") !== false) {
                    $match = strtr($match, ["'" => '"']);
                    $data = json_decode($match, true);
                }
                if (is_null($data) && mb_stripos($match, '\\') !== false) {
                    $match = strtr($match, ['\\' => '\\\\']);
                    $data = json_decode($match, true);
                }
//                if (! $data) $data = json_decode(str_replace("'", '"', $match), true);
                if (! is_null($data) ) {
                    $res = $data;
                    $res_length = $match_length;
                }
            }
//            }
        }
        if (is_null($res)) file_put_contents('format_json.txt', $content);
        return $res;
    }
    return $data;
}

function format_field_name($field)
{
    return '{{#'.$field.'#}}';
}


/**
 * 批量自动调用AI
 * @param $content
 * @param $model_names
 * @param $check_func
 * @param $stream_func
 * @param $messages
 * @param $timeout
 * @param $run_times
 * @return array|false|mixed
 * @author klinson <klinson@163.com>
 */
function auto_chat_bat($content, $model_names, $check_func = null, $run_start_func = null, $messages = [], $timeout = 1000, $run_times = 5)
{
    if (is_string($model_names)) $model_names = [$model_names];
    $real_times = 0;
    foreach ($model_names as $model_name) {
        $now_times = 1;
        while ($now_times++ <= $run_times) {
            $real_times++;
            if ($run_start_func) $run_start_func();
//            file_put_contents('aitest.json', json_encode($messages, JSON_UNESCAPED_UNICODE));
//            file_put_contents('aitest_content.txt', $content);
            $start_time = microtime(true);
            $res = auto_chat($content, $model_name, $messages, $timeout);
            if ($check_func) try {$res = $check_func($res);} catch (\Exception $e) {$res = false;}
            if ($res) return [
                'res' => $res,
                'ai_model' => $model_name,
                'run_times' => $real_times,
                'used_seconds' => round(microtime(true) - $start_time, 2),
            ];
        }
    }
    return false;
}

/**
 * 自动调用AI
 * @param $content
 * @param $model_name
 * @param $stream_func
 * @param $messages
 * @param $timeout
 * @return array|false
 * @author klinson <klinson@163.com>
 */
function auto_chat($content, $model_name, $messages = [], $timeout = 1000)
{
    if (mb_stripos($model_name, 'ms_api/') === 0) $res = modelscope_chat($content, $model_name, $messages, $timeout);
    else if (mb_stripos($model_name, 'deepseek_api/') === 0) $res = deepseek_chat($content, $model_name, $messages, $timeout);
    else $res = false;
    return $res;
}
